Skip to content

[volume-3] 도메인 & 객체 설계 및 아키텍처, 패키지 구성 - 김평숙#89

Closed
katiekim17 wants to merge 44 commits intoLoopers-dev-lab:katiekim17from
katiekim17:volume-3
Closed

[volume-3] 도메인 & 객체 설계 및 아키텍처, 패키지 구성 - 김평숙#89
katiekim17 wants to merge 44 commits intoLoopers-dev-lab:katiekim17from
katiekim17:volume-3

Conversation

@katiekim17
Copy link

@katiekim17 katiekim17 commented Feb 20, 2026

📌 Summary

  • 배경: 이커머스의 핵심 기능(브랜드, 상품, 좋아요, 주문)에 대한 구현이 요구됨.
    기존 Member 도메인은 User로의 변경과 VO를 추가해야 했음.

  • 목표: 파사드 패턴을 이용하여 DIP를 이해하고 이에 맞게 개발 진행. 각각의 역할과 책임에 대해 생각해보고 정리한다

  • 결과: 레이어드 아키텍처 기반으로 5개 도메인(User, Brand, Product, Order, Stock)을 구현하고, Domain 레이어에 Repository 인터페이스를 정의 / Infrastructure 레이어에서 구현하는 DIP 구조를 전 도메인에 적용함. Facade가 도메인 서비스를 조합하여 유스케이스를 오케스트레이션하는 패턴을 확립함

🧭 Context & Decision

문제 정의

  • 현재 동작/제약: 기존에는 Member 도메인만 존재하며, VO 없이 원시 타입으로 필드를 관리. 이커머스 핵심 기능(브랜드, 상품, 주문 등)이 미구현 상태
  • 문제(또는 리스크):
    • Member 도메인의 유효성 검증이 Service에 흩어져 있어 도메인 규칙이 보호되지 않음
    • 복수 도메인 간 의존 관계(브랜드→상품, 상품→재고→주문)가 없어 실제 이커머스 흐름 구현 불가
    • 어드민/사용자 API가 구분되지 않아 권한별 데이터 가시성 제어가 어려움
  • 성공 기준(완료 정의):
    • 전 도메인에 DIP가 적용되어 Domain이 Infrastructure에 의존하지 않을 것
    • 단위/통합/E2E 테스트가 모두 통과할 것
    • 어드민과 사용자 API가 Facade 수준에서 분리될 것

선택지와 결정

  • 고려한 대안:
    • A: Service 레이어에서 모든 유스케이스를 직접 처리 (Facade 없음)
    • B: Facade 패턴으로 유스케이스 오케스트레이션을 분리하고, Service는 단일 도메인 로직에 집중
    • 최종 결정: B안 채택. Facade가 복수 서비스를 조합(인증→조회→처리)하고, Service는 트랜잭션 경계 내에서 단일 도메인 비즈니스 로직을 담당
    • 트레이드오프:
      • 단일 도메인(Brand)의 경우 Facade가 Repository를 직접 호출하므로 Service 레이어를 생략 → 레이어가 간결하지만 도메인 복잡도가 올라가면 Service 분리가 필요할 수 있음
      • 주문 시 비관적 락으로 재고 차감 → 동시성 안전하지만 높은 트래픽에서 락 경합 가능
    • 추후 개선 여지:
      • 좋아요(Like) 도메인은 현재 인터페이스만 존재하며 구현 필요
      • 트래픽 증가 시 재고 차감을 Redis 기반 분산 락이나 메시지 큐로 전환 검토

🏗️ Design Overview

변경 범위

  • 영향 받는 모듈/도메인: commerce-api (interfaces, application, domain, infrastructure 전 레이어)
  • 신규 추가:
    • 도메인: Brand, Product(+Option/Image/History), Order(+OrderItem/ProductSnapshot), Stock, 공통 VO(Money, Quantity)
    • User VO: LoginId, Email, RawPassword, EncryptedPassword
    • 어드민 API: AdminController, AdminAuthInterceptor
    • 이벤트: BrandDeactivatedEvent, BrandEventListener
  • 제거/대체:
    • MemberModel → Users 엔티티로 대체
    • MemberService, MemberRepository, MemberRepositoryImpl → User 도메인으로 대체
    • MemberV1Controller, MemberV1Dto → UserV1Controller, UserV1Dto로 대체

주요 컴포넌트 책임

  • UserFacade: 회원가입/내정보조회/비밀번호변경의 유스케이스 조합. VO 검증은 도메인에 위임
  • AdminBrandFacade: 브랜드 CRUD + 비활성화 시 BrandDeactivatedEvent 발행
  • BrandEventListener: 브랜드 비활성화 커밋 후 비동기로 연관 상품 일괄 비활성화 + 이력 스냅샷 저장
  • ProductService: 상품 상세 조회 시 Brand/Option/Image를 조합하는 도메인 서비스
  • AdminProductFacade: 상품 CRUD + 등록/수정 시 ProductHistory 스냅샷 자동 생성 (버전 관리)
  • OrderFacade: 인증 → 상품 조회 → 브랜드 조회 → OrderService.createOrder() 호출의 오케스트레이션
  • OrderService: 재고 차감(StockDeductionService) → 주문 생성 → ProductSnapshot 포함 OrderItem 저장을 단일 트랜잭션으로 처리
  • StockDeductionService: productId 오름차순 정렬 후 비관적 락으로 재고 차감 (데드락 방지)

🔁 Flow Diagram

Main Flow - 주문 생성

  sequenceDiagram
    autonumber
    participant Client
    participant OrderController
    participant OrderFacade
    participant UserService
    participant ProductService
    participant OrderService
    participant StockDeductionService
    participant DB

    Client->>OrderController: POST /api/v1/orders
    OrderController->>OrderFacade: createOrder(loginId, password, items)
    OrderFacade->>UserService: authenticate(loginId, password)
    UserService->>DB: findByLoginId + passwordEncoder.matches
    DB-->>UserService: Users
    UserService-->>OrderFacade: Users

    OrderFacade->>ProductService: getProducts(productIds)
    ProductService->>DB: findAllByIds
    DB-->>OrderFacade: List<Product>

    OrderFacade->>ProductService: getBrands(brandIds)
    ProductService->>DB: findAllByIds
    DB-->>OrderFacade: List<Brand>

    OrderFacade->>OrderService: createOrder(userId, products, brandMap, deductionMap)
    OrderService->>StockDeductionService: deductAll(deductionMap) - sorted by productId
    StockDeductionService->>DB: findByProductIdWithLock (PESSIMISTIC_WRITE)
    DB-->>StockDeductionService: Stock (locked)
    StockDeductionService->>DB: stock.deduct(quantity)

    OrderService->>DB: save(Order)
    OrderService->>DB: saveAll(OrderItems + ProductSnapshot)
    DB-->>OrderService: Order
    OrderService-->>OrderFacade: Order
    OrderFacade-->>OrderController: OrderInfo
    OrderController-->>Client: ApiResponse<OrderInfo>
Loading

Exception Flow - 주문 생성

  sequenceDiagram
    autonumber
    participant Client
    participant OrderController
    participant OrderFacade
    participant UserService
    participant OrderService
    participant StockDeductionService
    participant DB

    Note over Client,DB: Case 1 - 인증 실패 (401)
    Client->>OrderController: POST /api/v1/orders
    OrderController->>OrderFacade: createOrder(loginId, password, items)
    OrderFacade->>UserService: authenticate(loginId, password)
    UserService->>DB: findByLoginId
    DB-->>UserService: Users
    UserService->>UserService: passwordEncoder.matches() == false
    UserService-->>OrderFacade: CoreException(UNAUTHORIZED)
    OrderFacade-->>Client: 401 Unauthorized

    Note over Client,DB: Case 2 - 존재하지 않는 상품 (404)
    Client->>OrderController: POST /api/v1/orders
    OrderController->>OrderFacade: createOrder(loginId, password, items)
    OrderFacade->>UserService: authenticate → OK
    OrderFacade->>OrderFacade: getProducts(productIds)
    OrderFacade-->>Client: CoreException(NOT_FOUND) → 404

    Note over Client,DB: Case 3 - 재고 부족 (400)
    Client->>OrderController: POST /api/v1/orders
    OrderController->>OrderFacade: createOrder(loginId, password, items)
    OrderFacade->>UserService: authenticate → OK
    OrderFacade->>OrderService: createOrder(...)
    OrderService->>StockDeductionService: deductAll(deductionMap)
    StockDeductionService->>DB: findByProductIdWithLock
    DB-->>StockDeductionService: Stock (quantity=5)
    StockDeductionService->>StockDeductionService: stock.deduct(Quantity(99999))
    StockDeductionService-->>OrderService: CoreException(BAD_REQUEST, "재고 부족")
    Note over OrderService,DB: 트랜잭션 롤백
    OrderService-->>Client: 400 Bad Request
Loading

Main Flow - 브랜드 비활성화 (이벤트 기반 연쇄 처리)

  sequenceDiagram
    autonumber
    participant Admin
    participant AdminController
    participant AdminBrandFacade
    participant DB
    participant EventPublisher
    participant BrandEventListener

    Admin->>AdminController: DELETE /api-admin/v1/brands/{id}
    AdminController->>AdminBrandFacade: deactivateBrand(brandId)
    AdminBrandFacade->>DB: findById(brandId)
    DB-->>AdminBrandFacade: Brand
    AdminBrandFacade->>AdminBrandFacade: brand.deactivate()
    AdminBrandFacade->>DB: save(brand)
    AdminBrandFacade->>EventPublisher: publish(BrandDeactivatedEvent)
    AdminBrandFacade-->>AdminController: void
    AdminController-->>Admin: 204 No Content

    Note over EventPublisher,BrandEventListener: AFTER_COMMIT + @Async (별도 트랜잭션, REQUIRES_NEW)
    EventPublisher->>BrandEventListener: handleBrandDeactivated(event)
    BrandEventListener->>DB: findAllByBrandId(brandId)
    DB-->>BrandEventListener: List<Product>
    loop 각 상품
      BrandEventListener->>BrandEventListener: product.deactivate()
      BrandEventListener->>DB: save(product)
      BrandEventListener->>DB: save(ProductHistory.snapshot(product, version, "system"))
    end
Loading

Exception Flow - 브랜드 비활성화

  sequenceDiagram
    autonumber
    participant Admin
    participant AdminController
    participant AdminBrandFacade
    participant DB

    Note over Admin,DB: Case 1 - LDAP 헤더 없음 (403)
    Admin->>AdminController: DELETE /api-admin/v1/brands/{id} (X-Loopers-Ldap 누락)
    AdminController->>AdminController: AdminAuthInterceptor 차단
    AdminController-->>Admin: 403 Forbidden

    Note over Admin,DB: Case 2 - 존재하지 않는 브랜드 (404)
    Admin->>AdminController: DELETE /api-admin/v1/brands/999999
    AdminController->>AdminBrandFacade: deactivateBrand(999999)
    AdminBrandFacade->>DB: findById(999999)
    DB-->>AdminBrandFacade: Optional.empty()
    AdminBrandFacade-->>Admin: CoreException(NOT_FOUND) → 404
Loading

katiekim17 and others added 20 commits February 5, 2026 01:01
- 회원가입 시퀀스 다이어그램 (핵심 + 예외 플로우)
- 내 정보 조회 시퀀스 다이어그램 (헤더 인증 포함)
- 비밀번호 변경 시퀀스 다이어그램 (핵심 + 예외 플로우)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
fix : 예제 테스트 코드 오류 해결을 위한 testcontainers 버전 업
Removed the version reference for User entity in requirements.
# Conflicts:
#	docs/design/브랜드_상품/01-requirements.md
#	docs/design/좋아요/01-requirements.md
[2주차] 설계 문서 제출 - 김평숙
@coderabbitai
Copy link

coderabbitai bot commented Feb 20, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@katiekim17 katiekim17 changed the title Volume 3 [volume-3] 도메인 & 객체 설계 및 아키텍처, 패키지 구성 - 김평숙 Feb 27, 2026
@katiekim17 katiekim17 closed this Mar 2, 2026
@katiekim17 katiekim17 deleted the volume-3 branch March 2, 2026 23:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants